﻿/*
 * This file is part of CommunitySettlers.
 *
 * CommunitySettlers is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * CommunitySettlers is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with CommunitySettlers.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Author: Christoph Husse
 * 
 * Also checkout our homepage: http://communitysettlers.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Drawing;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace SettlersEngine
{
    internal enum SettlerJobDirection
    {
        ProviderToQuery,
        ProviderToBuffer,
    }

    internal enum SettlerJobStep
    {
        Start,
        Pickup,
        Walk,
        Drop,
        Finish,
    }

    internal class SettlerJob
    {
        public SettlerJobStep Step { get; set; }
        public IResourceObject Provider { get; set; }
        public IResourceObject Query { get; set; }
        public IMovableObject Settler { get; set; }
        public SettlerJobDirection Direction { get; set; }

        public void Cancel()
        {
            if ((Settler != null) && (Settler.Job != null))
            {
                Settler.Job = null;
                Settler.Stop();
            }

            Provider = null;
            Query = null;
            Settler = null;
        }
    }

    [Serializable]
    internal enum ResourceObjectType
    {
        Provider,
        Query,
        Buffer,
    }

    [Serializable]
    internal class IResourceObject : IPositionProvider
    {
        public event DChangeHandler<IResourceObject, int> OnCountChanged;
        public event DNotifyHandler<IResourceObject> OnAvailableChanged;
        public event DNotifyHandler<IResourceObject> OnRequestedChanged;
        public event DChangeHandler<IResourceObject, Resource> OnResourceTypeChanged;
        public event DNotifyHandler<IResourceObject> OnIsEnabledChanged;

        private int m_Count;
        private int m_MaxStackSize;
        private Boolean m_IsEnabled = true;
        private Resource m_ResourceType;

        public Boolean IsEnabled
        {
            get { return m_IsEnabled; }
            set
            {
                if (m_IsEnabled == value)
                    return;

                m_IsEnabled = value;

                if (OnIsEnabledChanged != null)
                    OnIsEnabledChanged(this);
            }
        }
        public ResourceObjectType Type { get; private set; }
        public Resource ResourceType { 
            get { return m_ResourceType; }
            set
            {
                Resource backup = m_ResourceType;

                if (m_ResourceType == value)
                    return;

                m_ResourceType = value;

                if (OnResourceTypeChanged != null)
                    OnResourceTypeChanged(this, backup, m_ResourceType);
            }
        }
        public Boolean IsLowPriority { get; private set; }
        public Boolean IsReleasedOnEmpty { get; private set; }
        internal BindingList<SettlerJob> Jobs { get; private set; }
        internal LinkedListNode<IResourceObject> ProviderNode { get; set; }
        internal LinkedListNode<IResourceObject> QueryNode { get; set; }
        internal int QueryPriority { get; set; }
        internal int MaxStackSize
        {
            get
            {
                return m_MaxStackSize;
            }

            set
            {
                if ((value <= 0) || (value > RuntimeMap.MAX_RESOURCE_STACK))
                    throw new ArgumentOutOfRangeException();

                m_MaxStackSize = value;
            }
        }

        public Int32 Count
        {
            get { return m_Count; }
            set
            {
                int countBackup = Count;

                m_Count = value;

                if (OnAvailableChanged != null)
                    OnAvailableChanged(this);

                if (OnRequestedChanged != null)
                    OnRequestedChanged(this);

                if (OnCountChanged != null)
                    OnCountChanged(this, countBackup, Count);
            }
        }
        public Int32 Available
        {
            get
            {
                if (Type != ResourceObjectType.Query)
                    return Count - (from e in Jobs where e.Direction == SettlerJobDirection.ProviderToQuery select e).Count();
                else
                    return Count;
            }
        }
        public Int32 Requested
        {
            get
            {
                if (Type == ResourceObjectType.Provider)
                    return 0;
                else if (Type == ResourceObjectType.Buffer)
                    return MaxStackSize - Count - (from e in Jobs where e.Direction == SettlerJobDirection.ProviderToBuffer select e).Count();
                else // ResourceObjectType.Query
                    return MaxStackSize - Count - (from e in Jobs where e.Direction == SettlerJobDirection.ProviderToQuery select e).Count();
            }
        }

        [OnDeserialized]
        void OnDeserialized(StreamingContext ctx)
        {
            Jobs.ListChanged += OnJobsChanged;
        }

        private IResourceObject(RuntimeMap inParent, Resource inResType, CyclePoint inInitialPosition)
            : base(inParent, (int)inResType, inInitialPosition)
        {
            Jobs = new BindingList<SettlerJob>();
            ResourceType = inResType;
            Jobs.ListChanged += OnJobsChanged;
            MaxStackSize = RuntimeMap.MAX_RESOURCE_STACK;
        }

        private void OnJobsChanged(object sender, ListChangedEventArgs e)
        {
            switch (e.ListChangedType)
            {
                case ListChangedType.ItemDeleted:
                case ListChangedType.ItemAdded:
                    break;
                case ListChangedType.ItemChanged:
                    throw new InvalidOperationException("Setting is not supported on resource job list.");
                case ListChangedType.ItemMoved:
                    throw new InvalidOperationException("Moving is not supported on resource job list.");
                default:
                    return;
            }

            if (OnAvailableChanged != null)
                OnAvailableChanged(this);

            if (OnRequestedChanged != null)
                OnRequestedChanged(this);
        }


        internal static IResourceObject CreateAsDrop(RuntimeMap inParent, Resource inResType, Int32 inInitialCount, CyclePoint inInitialPosition)
        {
            IResourceObject result = new IResourceObject(inParent,inResType, inInitialPosition)
            {
                IsLowPriority = false,
                IsReleasedOnEmpty = true,
                Type = ResourceObjectType.Provider,
            };

            if (inInitialCount == 0)
                throw new ArgumentOutOfRangeException("A dropped resource can not be initialized with a zero stack size.");

            result.Count = inInitialCount;

            return result;
        }

        internal static IResourceObject CreateAsProvider(RuntimeMap inParent, Resource inResType, CyclePoint inInitialPosition)
        {
            return new IResourceObject(inParent, inResType, inInitialPosition)
            {
                IsLowPriority = false,
                IsReleasedOnEmpty = false,
                Type = ResourceObjectType.Provider,
            };
        }

        internal static IResourceObject CreateAsQuery(RuntimeMap inParent, Resource inResType, CyclePoint inInitialPosition)
        {
            return new IResourceObject(inParent,  inResType, inInitialPosition)
            {
                IsLowPriority = false,
                IsReleasedOnEmpty = false,
                Type = ResourceObjectType.Query,
            };
        }
    }

    internal class ResourceAnimationStep
    {
        public Int64 TimeOffset { get; internal set; }
        public IResourceObject Resource { get; internal set; }
        public StackDirection Direction { get; internal set; }
        public Int32 CycleCount { get; internal set; }
        public Int32 QualityIndex { get; internal set; }
    }

    /// <summary>
    /// A resource animation is an abstract concept to put resource stack size changes into
    /// a timeline. This is required for settlers dropping and fetching resources, but also
    /// for buildings to appropiately synchronize resource stacks with their production
    /// animation.
    /// </summary>
    internal class ResourceAnimation
    {
        private int m_NextStepIndex = 0;

        public ReadOnlyCollection<ResourceAnimationStep> Steps { get; internal set; }
        public Int64 StartTime { get; internal set; }
        public IPositionProvider Parent { get; internal set; }
        public Int64 ElapsedMillis { get { return (Int64)(Parent.Map.AnimationTime.Time.TotalMilliseconds - StartTime); } }
        public ResourceAnimationStep NextStep
        {
            get
            {
                if (m_NextStepIndex >= Steps.Count)
                    return null;

                return Steps[m_NextStepIndex];
            }
        }
        public event DNotifyHandler<IPositionProvider> OnCompleted;
        internal int CycleCounter { get; set; }

        internal ResourceAnimation(IPositionProvider inParent, IEnumerable<ResourceAnimationStep> inSteps, int inCycleCounter)
        {
            if ((inParent == null) || (inSteps == null))
                throw new ArgumentNullException();

            CycleCounter = inCycleCounter;
            Parent = inParent;
            StartTime = (Int64)Parent.Map.AnimationTime.Time.TotalMilliseconds;
            Steps = new ReadOnlyCollection<ResourceAnimationStep>(inSteps.ToList());
        }

        /// <summary>
        /// This method should be called by external code to indicate that resource
        /// operation is completed. 
        /// </summary>
        public void Completed()
        {
            if (NextStep != null)
            {
                // finish animation
                StartTime = Int64.MinValue / 2;

                Process();
            }

            if(OnCompleted != null)
                OnCompleted(Parent);
        }

        internal void Process()
        {
            if (NextStep == null)
                return;

            int minQualityIndex = 8;
            Boolean hasCycleStep = false;

            while ((NextStep != null) && (NextStep.TimeOffset <= ElapsedMillis))
            {
                try
                {
                    // process step
                    Boolean shouldDecrease = true;

                    minQualityIndex = NextStep.QualityIndex;

                    if (NextStep.CycleCount > 0)
                    {
                        if (hasCycleStep)
                            continue;

                        if ((CycleCounter % NextStep.CycleCount) != 0)
                            shouldDecrease = false;

                        if (NextStep.Resource.Count == 0)
                        {
                            /* 
                             * IBuildableObject.Produce() will take care that we only get here if all preconditions
                             * are met. So we prevent a stack underflow by assuming that a higher quality index will
                             * provide us with a resource...
                             */
                            shouldDecrease = false;
                        }
                        else
                            hasCycleStep = true;
                    }

                    NextStep.Resource.Count += (NextStep.Direction == StackDirection.Incoming) ? (shouldDecrease ? -1 : 0) : 1;
                }
                finally
                {
                    m_NextStepIndex++;
                }
            }
        }
    }

    internal partial class RuntimeMap
    {
        private readonly LinkedList<ResourceAnimation> m_ResourceAnimations = new LinkedList<ResourceAnimation>();

        /// <summary>
        /// Registers internal handlers for the given resource object. This is done
        /// in all the add-methods as well as during deserialization.
        /// </summary>
        /// <param name="inResource"></param>
        internal void RegisterNotifications(IResourceObject inResource)
        {
            inResource.OnAvailableChanged += OnResourceAvailableChanged;
            inResource.OnIsEnabledChanged += OnIsEnabledChanged;
            inResource.OnResourceTypeChanged += OnResourceTypeChanged;
        }

        internal void UnregisterNotifications(IResourceObject inResource)
        {
            inResource.OnAvailableChanged -= OnResourceAvailableChanged;
            inResource.OnIsEnabledChanged -= OnIsEnabledChanged;
            inResource.OnResourceTypeChanged -= OnResourceTypeChanged;
        }

        internal LinkedListNode<ResourceAnimation> RegisterResourceAnimation(ResourceAnimation inAnimation)
        {
            return m_ResourceAnimations.AddLast(inAnimation);
        }

        internal void UnregisterResourceAnimation(LinkedListNode<ResourceAnimation> inAnimation)
        {
            m_ResourceAnimations.Remove(inAnimation);
        }

        public void AddResource(IResourceObject inResource)
        {
            // register resource
            Grid.Register(inResource);

            RegisterNotifications(inResource);
        }

        private void OnResourceTypeChanged(IResourceObject inObject, Resource inOldType, Resource inNewType)
        {
            CancelResourceJobs(inObject);
        }

        private void OnIsEnabledChanged(IResourceObject inObject)
        {
            if (!inObject.IsEnabled)
                CancelResourceJobs(inObject);
        }

        internal void CancelResourceJobs(IResourceObject inObject)
        {
            foreach (var job in inObject.Jobs.ToArray())
            {
                job.Cancel();
            }
        }

        public void RemoveResource(IResourceObject inResource)
        {
            Grid.Unregister(inResource);

            UnregisterNotifications(inResource);

            inResource.Dispose();
        }

        private void OnResourceAvailableChanged(IResourceObject inObject)
        {

        }

        internal void ProcessResourceTransport(object sender, EventArgs args)
        {
            // TODO: create area mapping and store information for each area
            Boolean hasFreeSettlers = true;
            Boolean[] hasResourceProviders = new Boolean[(int)Resource.Max];

            for (int i = 0; i < hasResourceProviders.Length; i++)
            {
                hasResourceProviders[i] = true;
            }

            foreach (var prioQueue in Grid.ResourceQueryQueue)
            {
                foreach (var query in prioQueue)
                {
                    IMovableObject movable = null;
                    IResourceObject provider = null;

                    if (!hasFreeSettlers || !hasResourceProviders[(int)query.ResourceType])
                        continue;

                    // find provider around query
                    Grid.EnumResourceProviders(query.Position.ToPoint(), (prov) =>
                    {
                        if ((prov.ResourceType != query.ResourceType) || (prov.Available == 0))
                            return WalkResult.NotFound;

                        // find settler around provider
                        Grid.EnumSettlers(prov.GridPosition, (settler) =>
                        {
                            if ((settler.Type != MoveableType.Carrier) || (settler.Job != null))
                                return WalkResult.NotFound;

                            movable = settler;

                            return WalkResult.Success;
                        });

                        // it is important that provider is set BEFORE search is aborted because movable is NULL!!
                        provider = prov;

                        if (movable == null)
                            return WalkResult.Abort;

                        return WalkResult.Success;
                    });

                    if ((movable == null) || (provider == null))
                    {
                        if (provider == null)
                            hasResourceProviders[(int)query.ResourceType] = false;
                        else if (movable == null)
                            hasFreeSettlers = false;

                        continue;
                    }

                    // setup and register settler job
                    SettlerJob job = new SettlerJob()
                    {
                        Step = SettlerJobStep.Start,
                        Provider = provider,
                        Query = query,
                        Settler = movable,
                        Direction = (query.Type == ResourceObjectType.Buffer) ? SettlerJobDirection.ProviderToBuffer :
                            SettlerJobDirection.ProviderToQuery,
                    };

                    provider.Jobs.Add(job);
                    query.Jobs.Add(job);
                    movable.Job = job;

                    ProcessSettlerJobStep(movable);
                }
            }
        }

        internal void ProcessSettlerJobStep(IMovableObject inSettler)
        {
            if ((inSettler.Type != MoveableType.Carrier) || (inSettler.Job == null))
                return;

            SettlerJob job = (SettlerJob)inSettler.Job;

            switch (job.Step)
            {
                case SettlerJobStep.Start:
                    {
                        Grid.SetMovablePath(inSettler, job.Provider.GridPosition);

                        job.Step = SettlerJobStep.Walk;
                    }break;
                case SettlerJobStep.Pickup:
                    {
                        // TODO: play animation
                        job.Step = SettlerJobStep.Walk;
                    } break;
                case SettlerJobStep.Walk:
                    {
                        // TODO: both lines should be done after "Pickup"
                        job.Provider.Jobs.Remove(job);
                        job.Provider.Count--;

                        Grid.SetMovablePath(inSettler, job.Query.GridPosition);

                        job.Step = SettlerJobStep.Finish;
                    }break;
                case SettlerJobStep.Drop:
                    {
                        // TODO: play animation
                        job.Step = SettlerJobStep.Finish;
                    } break;
                case SettlerJobStep.Finish:
                    {
                        // TODO: both lines should be done after drop
                        job.Query.Jobs.Remove(job);
                        job.Query.Count++;

                        inSettler.Job = null;
                    } break;
            }
        }

        internal void ProcessResourceAnimation(object sender, EventArgs args)
        {
            foreach (var anim in m_ResourceAnimations)
            {
                anim.Process();
            }
        }
    }
}
